<?php
	/**
	 *	base include file for SimpleTest
	 *	@package	SimpleTest
	 *	@subpackage	UnitTester
	 *	@version	$Id$
	 */

	/**#@+
	 *	include other SimpleTest class files
	 */
	require_once(dirname(__FILE__) . '/scorer.php');
	/**#@-*/

	/**
	 *	Creates the XML needed for remote communication
	 *	by SimpleTest.
	 *	  @package SimpleTest
	 *	  @subpackage UnitTester
	 */
	class XmlReporter extends SimpleReporter {
		var $_indent;
		var $_namespace;

		/**
		 *	Sets up indentation and namespace.
		 *	@param string $namespace		Namespace to add to each tag.
		 *	@param string $indent		   Indenting to add on each nesting.
		 *	@access public
		 */
		function XmlReporter($namespace = false, $indent = '  ') {
			$this->SimpleReporter();
			$this->_namespace = ($namespace ? $namespace . ':' : '');
			$this->_indent = $indent;
		}

		/**
		 *	Calculates the pretty printing indent level
		 *	from the current level of nesting.
		 *	@param integer $offset  Extra indenting level.
		 *	@return string		  Leading space.
		 *	@access protected
		 */
		function _getIndent($offset = 0) {
			return str_repeat(
					$this->_indent,
					count($this->getTestList()) + $offset);
		}

		/**
		 *	Converts character string to parsed XML
		 *	entities string.
		 *	@param string text		Unparsed character data.
		 *	@return string			Parsed character data.
		 *	@access public
		 */
		function toParsedXml($text) {
			return str_replace(
					array('&', '<', '>', '"', '\''),
					array('&amp;', '&lt;', '&gt;', '&quot;', '&apos;'),
					$text);
		}

		/**
		 *	Paints the start of a group test.
		 *	@param string $test_name   Name of test that is starting.
		 *	@param integer $size	   Number of test cases starting.
		 *	@access public
		 */
		function paintGroupStart($test_name, $size) {
			parent::paintGroupStart($test_name, $size);
			print $this->_getIndent();
			print "<" . $this->_namespace . "group size=\"$size\">\n";
			print $this->_getIndent(1);
			print "<" . $this->_namespace . "name>" .
					$this->toParsedXml($test_name) .
					"</" . $this->_namespace . "name>\n";
		}

		/**
		 *	Paints the end of a group test.
		 *	@param string $test_name   Name of test that is ending.
		 *	@access public
		 */
		function paintGroupEnd($test_name) {
			print $this->_getIndent();
			print "</" . $this->_namespace . "group>\n";
			parent::paintGroupEnd($test_name);
		}

		/**
		 *	Paints the start of a test case.
		 *	@param string $test_name   Name of test that is starting.
		 *	@access public
		 */
		function paintCaseStart($test_name) {
			parent::paintCaseStart($test_name);
			print $this->_getIndent();
			print "<" . $this->_namespace . "case>\n";
			print $this->_getIndent(1);
			print "<" . $this->_namespace . "name>" .
					$this->toParsedXml($test_name) .
					"</" . $this->_namespace . "name>\n";
		}

		/**
		 *	Paints the end of a test case.
		 *	@param string $test_name   Name of test that is ending.
		 *	@access public
		 */
		function paintCaseEnd($test_name) {
			print $this->_getIndent();
			print "</" . $this->_namespace . "case>\n";
			parent::paintCaseEnd($test_name);
		}

		/**
		 *	Paints the start of a test method.
		 *	@param string $test_name   Name of test that is starting.
		 *	@access public
		 */
		function paintMethodStart($test_name) {
			parent::paintMethodStart($test_name);
			print $this->_getIndent();
			print "<" . $this->_namespace . "test>\n";
			print $this->_getIndent(1);
			print "<" . $this->_namespace . "name>" .
					$this->toParsedXml($test_name) .
					"</" . $this->_namespace . "name>\n";
		}

		/**
		 *	Paints the end of a test method.
		 *	@param string $test_name   Name of test that is ending.
		 *	@param integer $progress   Number of test cases ending.
		 *	@access public
		 */
		function paintMethodEnd($test_name) {
			print $this->_getIndent();
			print "</" . $this->_namespace . "test>\n";
			parent::paintMethodEnd($test_name);
		}

		/**
		 *	Paints pass as XML.
		 *	@param string $message		Message to encode.
		 *	@access public
		 */
		function paintPass($message) {
			parent::paintPass($message);
			print $this->_getIndent(1);
			print "<" . $this->_namespace . "pass>";
			print $this->toParsedXml($message);
			print "</" . $this->_namespace . "pass>\n";
		}

		/**
		 *	Paints failure as XML.
		 *	@param string $message		Message to encode.
		 *	@access public
		 */
		function paintFail($message) {
			parent::paintFail($message);
			print $this->_getIndent(1);
			print "<" . $this->_namespace . "fail>";
			print $this->toParsedXml($message);
			print "</" . $this->_namespace . "fail>\n";
		}

		/**
		 *	Paints error as XML.
		 *	@param string $message		Message to encode.
		 *	@access public
		 */
		function paintError($message) {
			parent::paintError($message);
			print $this->_getIndent(1);
			print "<" . $this->_namespace . "exception>";
			print $this->toParsedXml($message);
			print "</" . $this->_namespace . "exception>\n";
		}

		/**
		 *	Paints exception as XML.
		 *	@param Exception $exception	Exception to encode.
		 *	@access public
		 */
		function paintException($exception) {
			parent::paintException($exception);
			print $this->_getIndent(1);
			print "<" . $this->_namespace . "exception>";
			$message = 'Unexpected exception of type [' . get_class($exception) .
					'] with message ['. $exception->getMessage() .
					'] in ['. $exception->getFile() .
					' line ' . $exception->getLine() . ']';
			print $this->toParsedXml($message);
			print "</" . $this->_namespace . "exception>\n";
		}

		/**
		 *	Paints the skipping message and tag.
		 *	@param string $message		Text to display in skip tag.
		 *	@access public
		 */
		function paintSkip($message) {
			parent::paintSkip($message);
			print $this->_getIndent(1);
			print "<" . $this->_namespace . "skip>";
			print $this->toParsedXml($message);
			print "</" . $this->_namespace . "skip>\n";
		}

		/**
		 *	Paints a simple supplementary message.
		 *	@param string $message		Text to display.
		 *	@access public
		 */
		function paintMessage($message) {
			parent::paintMessage($message);
			print $this->_getIndent(1);
			print "<" . $this->_namespace . "message>";
			print $this->toParsedXml($message);
			print "</" . $this->_namespace . "message>\n";
		}

		/**
		 *	Paints a formatted ASCII message such as a
		 *	variable dump.
		 *	@param string $message		Text to display.
		 *	@access public
		 */
		function paintFormattedMessage($message) {
			parent::paintFormattedMessage($message);
			print $this->_getIndent(1);
			print "<" . $this->_namespace . "formatted>";
			print "<![CDATA[$message]]>";
			print "</" . $this->_namespace . "formatted>\n";
		}

		/**
		 *	Serialises the event object.
		 *	@param string $type		Event type as text.
		 *	@param mixed $payload	  Message or object.
		 *	@access public
		 */
		function paintSignal($type, &$payload) {
			parent::paintSignal($type, $payload);
			print $this->_getIndent(1);
			print "<" . $this->_namespace . "signal type=\"$type\">";
			print "<![CDATA[" . serialize($payload) . "]]>";
			print "</" . $this->_namespace . "signal>\n";
		}

		/**
		 *	Paints the test document header.
		 *	@param string $test_name	 First test top level
		 *								 to start.
		 *	@access public
		 *	@abstract
		 */
		function paintHeader($test_name) {
			if (! SimpleReporter::inCli()) {
				header('Content-type: text/xml');
			}
			print "<?xml version=\"1.0\"";
			if ($this->_namespace) {
				print " xmlns:" . $this->_namespace .
						"=\"www.lastcraft.com/SimpleTest/Beta3/Report\"";
			}
			print "?>\n";
			print "<" . $this->_namespace . "run>\n";
		}

		/**
		 *	Paints the test document footer.
		 *	@param string $test_name		The top level test.
		 *	@access public
		 *	@abstract
		 */
		function paintFooter($test_name) {
			print "</" . $this->_namespace . "run>\n";
		}
	}

	/**
	 *	Accumulator for incoming tag. Holds the
	 *	incoming test structure information for
	 *	later dispatch to the reporter.
	 *	  @package SimpleTest
	 *	  @subpackage UnitTester
	 */
	class NestingXmlTag {
		var $_name;
		var $_attributes;

		/**
		 *	Sets the basic test information except
		 *	the name.
		 *	@param hash $attributes   Name value pairs.
		 *	@access public
		 */
		function NestingXmlTag($attributes) {
			$this->_name = false;
			$this->_attributes = $attributes;
		}

		/**
		 *	Sets the test case/method name.
		 *	@param string $name		Name of test.
		 *	@access public
		 */
		function setName($name) {
			$this->_name = $name;
		}

		/**
		 *	Accessor for name.
		 *	@return string		Name of test.
		 *	@access public
		 */
		function getName() {
			return $this->_name;
		}

		/**
		 *	Accessor for attributes.
		 *	@return hash		All attributes.
		 *	@access protected
		 */
		function _getAttributes() {
			return $this->_attributes;
		}
	}

	/**
	 *	Accumulator for incoming method tag. Holds the
	 *	incoming test structure information for
	 *	later dispatch to the reporter.
	 *	  @package SimpleTest
	 *	  @subpackage UnitTester
	 */
	class NestingMethodTag extends NestingXmlTag {

		/**
		 *	Sets the basic test information except
		 *	the name.
		 *	@param hash $attributes   Name value pairs.
		 *	@access public
		 */
		function NestingMethodTag($attributes) {
			$this->NestingXmlTag($attributes);
		}

		/**
		 *	Signals the appropriate start event on the
		 *	listener.
		 *	@param SimpleReporter $listener	Target for events.
		 *	@access public
		 */
		function paintStart(&$listener) {
			$listener->paintMethodStart($this->getName());
		}

		/**
		 *	Signals the appropriate end event on the
		 *	listener.
		 *	@param SimpleReporter $listener	Target for events.
		 *	@access public
		 */
		function paintEnd(&$listener) {
			$listener->paintMethodEnd($this->getName());
		}
	}

	/**
	 *	Accumulator for incoming case tag. Holds the
	 *	incoming test structure information for
	 *	later dispatch to the reporter.
	 *	  @package SimpleTest
	 *	  @subpackage UnitTester
	 */
	class NestingCaseTag extends NestingXmlTag {

		/**
		 *	Sets the basic test information except
		 *	the name.
		 *	@param hash $attributes   Name value pairs.
		 *	@access public
		 */
		function NestingCaseTag($attributes) {
			$this->NestingXmlTag($attributes);
		}

		/**
		 *	Signals the appropriate start event on the
		 *	listener.
		 *	@param SimpleReporter $listener	Target for events.
		 *	@access public
		 */
		function paintStart(&$listener) {
			$listener->paintCaseStart($this->getName());
		}

		/**
		 *	Signals the appropriate end event on the
		 *	listener.
		 *	@param SimpleReporter $listener	Target for events.
		 *	@access public
		 */
		function paintEnd(&$listener) {
			$listener->paintCaseEnd($this->getName());
		}
	}

	/**
	 *	Accumulator for incoming group tag. Holds the
	 *	incoming test structure information for
	 *	later dispatch to the reporter.
	 *	  @package SimpleTest
	 *	  @subpackage UnitTester
	 */
	class NestingGroupTag extends NestingXmlTag {

		/**
		 *	Sets the basic test information except
		 *	the name.
		 *	@param hash $attributes   Name value pairs.
		 *	@access public
		 */
		function NestingGroupTag($attributes) {
			$this->NestingXmlTag($attributes);
		}

		/**
		 *	Signals the appropriate start event on the
		 *	listener.
		 *	@param SimpleReporter $listener	Target for events.
		 *	@access public
		 */
		function paintStart(&$listener) {
			$listener->paintGroupStart($this->getName(), $this->getSize());
		}

		/**
		 *	Signals the appropriate end event on the
		 *	listener.
		 *	@param SimpleReporter $listener	Target for events.
		 *	@access public
		 */
		function paintEnd(&$listener) {
			$listener->paintGroupEnd($this->getName());
		}

		/**
		 *	The size in the attributes.
		 *	@return integer	 Value of size attribute or zero.
		 *	@access public
		 */
		function getSize() {
			$attributes = $this->_getAttributes();
			if (isset($attributes['SIZE'])) {
				return (integer)$attributes['SIZE'];
			}
			return 0;
		}
	}

	/**
	 *	Parser for importing the output of the XmlReporter.
	 *	Dispatches that output to another reporter.
	 *	  @package SimpleTest
	 *	  @subpackage UnitTester
	 */
	class SimpleTestXmlParser {
		var $_listener;
		var $_expat;
		var $_tag_stack;
		var $_in_content_tag;
		var $_content;
		var $_attributes;

		/**
		 *	Loads a listener with the SimpleReporter
		 *	interface.
		 *	@param SimpleReporter $listener   Listener of tag events.
		 *	@access public
		 */
		function SimpleTestXmlParser(&$listener) {
			$this->_listener = &$listener;
			$this->_expat = &$this->_createParser();
			$this->_tag_stack = array();
			$this->_in_content_tag = false;
			$this->_content = '';
			$this->_attributes = array();
		}

		/**
		 *	Parses a block of XML sending the results to
		 *	the listener.
		 *	@param string $chunk		Block of text to read.
		 *	@return boolean			 True if valid XML.
		 *	@access public
		 */
		function parse($chunk) {
			if (! xml_parse($this->_expat, $chunk)) {
				trigger_error('XML parse error with ' .
						xml_error_string(xml_get_error_code($this->_expat)));
				return false;
			}
			return true;
		}

		/**
		 *	Sets up expat as the XML parser.
		 *	@return resource		Expat handle.
		 *	@access protected
		 */
		function &_createParser() {
			$expat = xml_parser_create();
			xml_set_object($expat, $this);
			xml_set_element_handler($expat, '_startElement', '_endElement');
			xml_set_character_data_handler($expat, '_addContent');
			xml_set_default_handler($expat, '_default');
			return $expat;
		}

		/**
		 *	Opens a new test nesting level.
		 *	@return NestedXmlTag	 The group, case or method tag
		 *							 to start.
		 *	@access private
		 */
		function _pushNestingTag($nested) {
			array_unshift($this->_tag_stack, $nested);
		}

		/**
		 *	Accessor for current test structure tag.
		 *	@return NestedXmlTag	 The group, case or method tag
		 *							 being parsed.
		 *	@access private
		 */
		function &_getCurrentNestingTag() {
			return $this->_tag_stack[0];
		}

		/**
		 *	Ends a nesting tag.
		 *	@return NestedXmlTag	 The group, case or method tag
		 *							 just finished.
		 *	@access private
		 */
		function _popNestingTag() {
			return array_shift($this->_tag_stack);
		}

		/**
		 *	Test if tag is a leaf node with only text content.
		 *	@param string $tag		XML tag name.
		 *	@return @boolean		  True if leaf, false if nesting.
		 *	@private
		 */
		function _isLeaf($tag) {
			return in_array($tag, array(
					'NAME', 'PASS', 'FAIL', 'EXCEPTION', 'SKIP', 'MESSAGE', 'FORMATTED', 'SIGNAL'));
		}

		/**
		 *	Handler for start of event element.
		 *	@param resource $expat	 Parser handle.
		 *	@param string $tag		 Element name.
		 *	@param hash $attributes	Name value pairs.
		 *							   Attributes without content
		 *							   are marked as true.
		 *	@access protected
		 */
		function _startElement($expat, $tag, $attributes) {
			$this->_attributes = $attributes;
			if ($tag == 'GROUP') {
				$this->_pushNestingTag(new NestingGroupTag($attributes));
			} elseif ($tag == 'CASE') {
				$this->_pushNestingTag(new NestingCaseTag($attributes));
			} elseif ($tag == 'TEST') {
				$this->_pushNestingTag(new NestingMethodTag($attributes));
			} elseif ($this->_isLeaf($tag)) {
				$this->_in_content_tag = true;
				$this->_content = '';
			}
		}

		/**
		 *	End of element event.
		 *	@param resource $expat	 Parser handle.
		 *	@param string $tag		 Element name.
		 *	@access protected
		 */
		function _endElement($expat, $tag) {
			$this->_in_content_tag = false;
			if (in_array($tag, array('GROUP', 'CASE', 'TEST'))) {
				$nesting_tag = $this->_popNestingTag();
				$nesting_tag->paintEnd($this->_listener);
			} elseif ($tag == 'NAME') {
				$nesting_tag = &$this->_getCurrentNestingTag();
				$nesting_tag->setName($this->_content);
				$nesting_tag->paintStart($this->_listener);
			} elseif ($tag == 'PASS') {
				$this->_listener->paintPass($this->_content);
			} elseif ($tag == 'FAIL') {
				$this->_listener->paintFail($this->_content);
			} elseif ($tag == 'EXCEPTION') {
				$this->_listener->paintError($this->_content);
			} elseif ($tag == 'SKIP') {
				$this->_listener->paintSkip($this->_content);
			} elseif ($tag == 'SIGNAL') {
				$this->_listener->paintSignal(
						$this->_attributes['TYPE'],
						unserialize($this->_content));
			} elseif ($tag == 'MESSAGE') {
				$this->_listener->paintMessage($this->_content);
			} elseif ($tag == 'FORMATTED') {
				$this->_listener->paintFormattedMessage($this->_content);
			}
		}

		/**
		 *	Content between start and end elements.
		 *	@param resource $expat	 Parser handle.
		 *	@param string $text		Usually output messages.
		 *	@access protected
		 */
		function _addContent($expat, $text) {
			if ($this->_in_content_tag) {
				$this->_content .= $text;
			}
			return true;
		}

		/**
		 *	XML and Doctype handler. Discards all such content.
		 *	@param resource $expat	 Parser handle.
		 *	@param string $default	 Text of default content.
		 *	@access protected
		 */
		function _default($expat, $default) {
		}
	}
?>
